// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(require('../../lib/codemirror'));
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['../../lib/codemirror'], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  CodeMirror.defineMode('elm', function() {
    function switchState(source, setState, f) {
      setState(f);
      return f(source, setState);
    }

    // These should all be Unicode extended, as per the Haskell 2010 report
    var smallRE = /[a-z_]/;
    var largeRE = /[A-Z]/;
    var digitRE = /[0-9]/;
    var hexitRE = /[0-9A-Fa-f]/;
    var octitRE = /[0-7]/;
    var idRE = /[a-z_A-Z0-9\']/;
    var symbolRE = /[-!#$%&*+.\/<=>?@\\^|~:\u03BB\u2192]/;
    var specialRE = /[(),;[\]`{}]/;
    var whiteCharRE = /[ \t\v\f]/; // newlines are handled in tokenizer

    function normal() {
      return function(source, setState) {
        if (source.eatWhile(whiteCharRE)) {
          return null;
        }

        var ch = source.next();
        if (specialRE.test(ch)) {
          if (ch == '{' && source.eat('-')) {
            var t = 'comment';
            if (source.eat('#')) t = 'meta';
            return switchState(source, setState, ncomment(t, 1));
          }
          return null;
        }

        if (ch == "'") {
          if (source.eat('\\')) source.next();
          // should handle other escapes here
          else source.next();

          if (source.eat("'")) return 'string';
          return 'error';
        }

        if (ch == '"') {
          return switchState(source, setState, stringLiteral);
        }

        if (largeRE.test(ch)) {
          source.eatWhile(idRE);
          if (source.eat('.')) return 'qualifier';
          return 'variable-2';
        }

        if (smallRE.test(ch)) {
          var isDef = source.pos === 1;
          source.eatWhile(idRE);
          return isDef ? 'type' : 'variable';
        }

        if (digitRE.test(ch)) {
          if (ch == '0') {
            if (source.eat(/[xX]/)) {
              source.eatWhile(hexitRE); // should require at least 1
              return 'integer';
            }
            if (source.eat(/[oO]/)) {
              source.eatWhile(octitRE); // should require at least 1
              return 'number';
            }
          }
          source.eatWhile(digitRE);
          var t = 'number';
          if (source.eat('.')) {
            t = 'number';
            source.eatWhile(digitRE); // should require at least 1
          }
          if (source.eat(/[eE]/)) {
            t = 'number';
            source.eat(/[-+]/);
            source.eatWhile(digitRE); // should require at least 1
          }
          return t;
        }

        if (symbolRE.test(ch)) {
          if (ch == '-' && source.eat(/-/)) {
            source.eatWhile(/-/);
            if (!source.eat(symbolRE)) {
              source.skipToEnd();
              return 'comment';
            }
          }
          source.eatWhile(symbolRE);
          return 'builtin';
        }

        return 'error';
      };
    }

    function ncomment(type, nest) {
      if (nest == 0) {
        return normal();
      }
      return function(source, setState) {
        var currNest = nest;
        while (!source.eol()) {
          var ch = source.next();
          if (ch == '{' && source.eat('-')) {
            ++currNest;
          } else if (ch == '-' && source.eat('}')) {
            --currNest;
            if (currNest == 0) {
              setState(normal());
              return type;
            }
          }
        }
        setState(ncomment(type, currNest));
        return type;
      };
    }

    function stringLiteral(source, setState) {
      while (!source.eol()) {
        var ch = source.next();
        if (ch == '"') {
          setState(normal());
          return 'string';
        }
        if (ch == '\\') {
          if (source.eol() || source.eat(whiteCharRE)) {
            setState(stringGap);
            return 'string';
          }
          if (!source.eat('&')) source.next(); // should handle other escapes here
        }
      }
      setState(normal());
      return 'error';
    }

    function stringGap(source, setState) {
      if (source.eat('\\')) {
        return switchState(source, setState, stringLiteral);
      }
      source.next();
      setState(normal());
      return 'error';
    }

    var wellKnownWords = (function() {
      var wkw = {};

      var keywords = [
        'case',
        'of',
        'as',
        'if',
        'then',
        'else',
        'let',
        'in',
        'infix',
        'infixl',
        'infixr',
        'type',
        'alias',
        'input',
        'output',
        'foreign',
        'loopback',
        'module',
        'where',
        'import',
        'exposing',
        '_',
        '..',
        '|',
        ':',
        '=',
        '\\',
        '"',
        '->',
        '<-',
      ];

      for (var i = keywords.length; i--; ) wkw[keywords[i]] = 'keyword';

      return wkw;
    })();

    return {
      startState: function() {
        return { f: normal() };
      },
      copyState: function(s) {
        return { f: s.f };
      },

      token: function(stream, state) {
        var t = state.f(stream, function(s) {
          state.f = s;
        });
        var w = stream.current();
        return wellKnownWords.hasOwnProperty(w) ? wellKnownWords[w] : t;
      },
    };
  });

  CodeMirror.defineMIME('text/x-elm', 'elm');
});
